Skip to content

feat: implement client template management system #286

Open
ImMohammad20000 wants to merge 12 commits intodevfrom
core-templates
Open

feat: implement client template management system #286
ImMohammad20000 wants to merge 12 commits intodevfrom
core-templates

Conversation

@ImMohammad20000
Copy link
Contributor

@ImMohammad20000 ImMohammad20000 commented Feb 22, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added core template management system with full CRUD functionality via new API endpoints
    • Support for creating, modifying, and deleting subscription templates (Clash, Xray, Singbox, user-agent formats)
    • Ability to set and toggle default templates per template type
    • Template search, filtering, and pagination capabilities
    • Templates now persist in database with proper default handling and version control
  • Chores

    • Migrated template storage from static files to database-backed system for improved flexibility

- Removed legacy XRAY subscription template file and related configurations.
- Introduced a new CoreTemplate model with CRUD operations for managing core templates.
- Added API endpoints for creating, retrieving, modifying, and deleting core templates.
- Implemented validation for template content based on type (e.g., clash, xray, singbox).
- Created default templates for clash, xray, singbox, user-agent, and gRPC user-agent.
- Added unit tests for core template creation, retrieval, and deletion logic.
@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

Walkthrough

This PR introduces a database-driven Core Templates system that moves subscription templates (Clash, Xray, SingBox) and user-agent configurations from static files to persistent database storage. It includes CRUD operations, API endpoints with admin access control, database migration, and cache synchronization via a message bus.

Changes

Cohort / File(s) Summary
Database Model & Schema
app/db/models.py, app/db/migrations/versions/e8c6a4f1d2b7_add_core_templates_table.py
Introduces CoreTemplate ORM model with id, name, template_type, content, is_default, is_system fields and composite-unique constraint. Migration creates table with default templates (clash, xray, singbox, user_agent, grpc_user_agent) populated from environment or default sources.
CRUD Layer
app/db/crud/core_template.py, app/db/crud/__init__.py
Implements comprehensive CRUD operations: fetch by id/type, create with default handling, modify with conflict resolution, delete with system/last-template guards. Includes merge logic for default values and helper functions for template content resolution.
Data Models
app/models/core_template.py
Defines Pydantic models: CoreTemplateType enum, CoreTemplateBase/Create/Modify/Response classes with validators, and simple response containers. Supports attribute-style access via ConfigDict.
Operation Layer
app/operation/__init__.py, app/operation/core_template.py
Adds BaseOperation.get_validated_core_template helper and CoreTemplateOperation class with lifecycle management, cache sync, and content validation for different template types.
API Endpoints & Routing
app/routers/core_template.py, app/routers/__init__.py
New FastAPI router with 6 endpoints (POST/PUT/DELETE/GET) requiring sudo admin, plus listing with optional filters. Delegates to CoreTemplateOperation and enforces auth via dependencies.
Message Bus Integration
app/nats/message.py, app/subscription/core_templates.py, app/app_factory.py
Adds MessageTopic.CORE_TEMPLATE enum member, cache helper functions (subscription_core_templates, refresh_core_templates_cache, handle_core_template_message), and registers NATS handler in app factory for cache invalidation.
Template Defaults
app/subscription/default_templates.py
Extracted default template strings for Clash, Xray, SingBox, user-agent, and gRPC user-agent into a new module with legacy-key mapping for backwards compatibility.
Subscription System Refactoring
app/subscription/base.py, app/subscription/clash.py, app/subscription/xray.py, app/subscription/singbox.py, app/subscription/links.py
BaseSubscription now accepts optional user_agent/grpc_user_agent template content parameters. ClashConfiguration, XrayConfiguration, SingBoxConfiguration updated to accept and use dynamic template content via render_template_string instead of static files.
Subscription Generation
app/subscription/share.py
Refactored to fetch core_templates cache, build subscription configs dynamically with template content, and thread core_templates through process_inbounds_and_tags and _prepare_download_settings for template-based content injection.
Template Utilities
app/templates/__init__.py
Added render_template_string utility to render Jinja2 templates from string content instead of files only.
Configuration & Cleanup
.env.example, config.py, app/templates/clash/default.yml, app/templates/xray/default.json, app/templates/singbox/default.json, app/templates/user_agent/grpc.json
Removed CLASH/XRAY/SINGBOX/USER_AGENT template config variables and deleted corresponding template files. Updated .env.example with DB table documentation.
Testing
tests/api/helpers.py, tests/api/test_core_template.py
Added create_core_template and delete_core_template test helpers; added 3 API tests covering create/get, default-switching, and delete guards (system/last-template protection).
Subscription Signature Update
app/operation/subscription.py
Minor formatting change to fetch_config signature (line wrapping only); no functional changes.

Sequence Diagram

sequenceDiagram
    participant Admin as Admin User
    participant API as API Router
    participant Op as CoreTemplateOperation
    participant DB as Database
    participant Cache as Cache Layer
    participant Bus as Message Bus
    participant Sub as Subscription Generator

    Admin->>API: PUT /api/core_template/{id}
    API->>Op: modify_core_template(db, id, changes, admin)
    Op->>DB: Update template content
    Op->>Cache: _sync_core_template_cache()
    Cache->>Bus: Publish CORE_TEMPLATE message
    Bus->>Cache: handle_core_template_message()
    Cache->>Cache: refresh_core_templates_cache()
    
    Note over Sub: Next subscription request
    Sub->>Cache: subscription_core_templates()
    Cache-->>Sub: Return updated templates
    Sub->>Sub: Build configs with new content
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 Templates hop from files to DB with glee,
Cache syncs via NATS, so fresh and so free!
Admin controls the defaults with care,
Dynamic content everywhere, everywhere!
Five new endpoints, guards keeping all safe,
This template migration sets the right pace! 🌟

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: implement client template management system' is unclear due to 'client' being ambiguous—it doesn't clearly distinguish this from other template systems. However, it does relate to the main objective of implementing a core template management system with CRUD operations and API endpoints. Consider revising the title to 'feat: implement core template management system' for clarity and consistency with PR objectives, which consistently refer to 'core templates' rather than 'client templates'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch core-templates

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ImMohammad20000 ImMohammad20000 added enhancement New feature or request Backend labels Feb 22, 2026
@ImMohammad20000 ImMohammad20000 marked this pull request as draft February 22, 2026 10:18
@ImMohammad20000
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/subscription/singbox.py (1)

56-72: ⚠️ Potential issue | 🔴 Critical

Pipeline failure: KeyError: 'type' when outbound entries lack a "type" field.

Now that templates are user-configurable, outbound["type"] on Lines 58/60 will raise KeyError if any outbound in the loaded config is missing the "type" key. Previously, the static template guaranteed this field. Use .get("type") to guard against malformed entries.

🐛 Proposed fix
     def render(self, reverse=False):
         urltest_types = ["vmess", "vless", "trojan", "shadowsocks", "hysteria2", "tuic", "http", "ssh"]
-        urltest_tags = [outbound["tag"] for outbound in self.config["outbounds"] if outbound["type"] in urltest_types]
+        urltest_tags = [outbound["tag"] for outbound in self.config["outbounds"] if outbound.get("type") in urltest_types]
         selector_types = ["vmess", "vless", "trojan", "shadowsocks", "hysteria2", "tuic", "http", "ssh", "urltest"]
-        selector_tags = [outbound["tag"] for outbound in self.config["outbounds"] if outbound["type"] in selector_types]
+        selector_tags = [outbound["tag"] for outbound in self.config["outbounds"] if outbound.get("type") in selector_types]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/subscription/singbox.py` around lines 56 - 72, In render(), the list
comprehensions building urltest_tags and selector_tags access outbound["type"]
which raises KeyError for outbounds missing the "type" key; change those to use
outbound.get("type") (e.g., in the comprehensions that create urltest_tags and
selector_tags) so malformed entries are skipped safely and behavior remains the
same; adjust any similar direct dict indexing in render() to .get("type") to
prevent the crash.
🧹 Nitpick comments (21)
app/subscription/core_templates.py (1)

7-10: Consider adding a TTL to @cached() as a safety net against stale data.

@cached() with no ttl argument means the cached value never expires — invalidation relies entirely on NATS messages reaching handle_core_template_message. If NATS delivery fails or is delayed, stale template data persists indefinitely.

Adding a modest TTL (e.g., 5–10 minutes) provides a safety net:

♻️ Suggested change
-@cached()
+@cached(ttl=300)
 async def subscription_core_templates() -> dict[str, str]:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/subscription/core_templates.py` around lines 7 - 10, The cached
subscription_core_templates function currently uses `@cached`() with no ttl so
values never expire; add a modest TTL (e.g., ttl=300 or ttl=600) to the `@cached`
decorator on subscription_core_templates to ensure the cache auto-invalidates
periodically as a safety net if NATS messages to handle_core_template_message
are missed or delayed, while keeping get_core_template_values and the existing
manual invalidation logic intact (use the same decorator signature where
subscription_core_templates is defined).
app/subscription/default_templates.py (1)

18-22: Hardcoded region-specific DNS servers in default template.

The proxy-server-nameserver entries (178.22.122.100, 78.157.42.100) are Shecan DNS servers specific to Iran. If this panel is intended for broader use, consider using more universally appropriate defaults (e.g., 1.1.1.1, 8.8.8.8) or documenting these as region-specific.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/subscription/default_templates.py` around lines 18 - 22, The default
template contains hardcoded region-specific DNS entries under the key
"proxy-server-nameserver" (values 178.22.122.100 and 78.157.42.100); update the
template in app/subscription/default_templates.py to use globally appropriate
defaults (for example 1.1.1.1 and 8.8.8.8) or replace these values with clearly
documented region-specific placeholders and add a comment/README note indicating
they are Iran-specific so operators can override them; locate the "nameserver"
and "proxy-server-nameserver" entries in the template and change the values or
add the documentation accordingly.
app/db/models.py (1)

717-729: Model looks well-structured.

The composite unique constraint and type index align with the query patterns in the CRUD layer. Using String(32) for template_type (instead of SQLEnum) defers validation to the application layer, which is flexible.

One optional observation: unlike most other models in this file (Admin, Node, CoreConfig, etc.), CoreTemplate lacks a created_at timestamp. If auditing template creation/modification times is useful, consider adding one.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/db/models.py` around lines 717 - 729, CoreTemplate is missing a
created_at timestamp—add a Mapped[datetime] mapped_column named created_at to
the CoreTemplate model (class CoreTemplate) so creation time is recorded;
implement it with a non-nullable DateTime column using a server default (e.g.,
func.now()) to auto-populate, and ensure you import DateTime and func from
sqlalchemy if not already present.
app/db/crud/core_template.py (6)

134-141: isinstance(s.value, tuple) branch is dead code.

CoreTemplateSortingOptionsSimple values are SQLAlchemy UnaryExpression objects (produced by .asc() / .desc()), never tuples. The sort_list.extend(s.value) path is unreachable and can be simplified away.

♻️ Proposed simplification
     if sort:
-        sort_list = []
-        for s in sort:
-            if isinstance(s.value, tuple):
-                sort_list.extend(s.value)
-            else:
-                sort_list.append(s.value)
-        stmt = stmt.order_by(*sort_list)
+        stmt = stmt.order_by(*[s.value for s in sort])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/db/crud/core_template.py` around lines 134 - 141, The tuple branch is
dead because CoreTemplateSortingOptionsSimple items yield SQLAlchemy
UnaryExpression objects, not tuples; update the sorting logic in
core_template.py to always collect s.value (e.g., replace the isinstance branch
with a simple comprehension or single append per item) so sort_list becomes
[s.value for s in sort] and then call stmt = stmt.order_by(*sort_list);
reference symbols: sort, sort_list, s.value, and stmt.order_by.

134-141: Dead branch: isinstance(s.value, tuple) is never True.

CoreTemplateSortingOptionsSimple values are SQLAlchemy UnaryExpression objects (produced by .asc() / .desc()), never tuples. The sort_list.extend(s.value) path is unreachable and can be removed.

♻️ Proposed simplification
     if sort:
-        sort_list = []
-        for s in sort:
-            if isinstance(s.value, tuple):
-                sort_list.extend(s.value)
-            else:
-                sort_list.append(s.value)
-        stmt = stmt.order_by(*sort_list)
+        stmt = stmt.order_by(*[s.value for s in sort])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/db/crud/core_template.py` around lines 134 - 141, The branch checking
isinstance(s.value, tuple) is dead because CoreTemplateSortingOptionsSimple
produces SQLAlchemy UnaryExpression objects (from .asc()/.desc()), so remove the
unreachable tuple branch and simplify the loop in the sort handling: build
sort_list by appending s.value for each s in sort and then call
stmt.order_by(*sort_list); update the block around sort, sort_list, s.value, and
stmt.order_by accordingly to eliminate the isinstance(...) branch.

62-64: Silent DB failure in get_core_template_values emits no log.

When the SELECT fails with SQLAlchemyError, the function silently falls back to hardcoded defaults with no warning. Operators would have no visibility that the database is unreachable and all subscribers are being served stale defaults.

♻️ Proposed fix — add a warning log
     except SQLAlchemyError:
+        logger.warning("Failed to fetch core template values from DB; falling back to defaults", exc_info=True)
         return defaults

(Import or reuse a module-level logger, e.g. logger = get_logger("core-template-crud"))

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/db/crud/core_template.py` around lines 62 - 64, The catch in
get_core_template_values currently swallows SQLAlchemyError and returns defaults
silently; update the function to log a warning with the exception details before
returning defaults by obtaining/using a module-level logger (e.g. define logger
= get_logger("core-template-crud") at top or reuse an existing logger) and
change the except block to log the error/stack (include the exception object)
and then return defaults so operators can see DB failures.

108-111: if limit: skips the limit clause when limit=0, returning all rows instead of none.

Using truthiness on an int parameter treats 0 as falsy. For offset=0 this is a no-op (harmless), but limit=0 silently drops the .limit(0) call, returning the full result set. The same pattern appears in get_core_templates_simple (lines 148–151).

♻️ Proposed fix (apply identically in get_core_templates_simple lines 148–151)
-    if offset:
+    if offset is not None:
         query = query.offset(offset)
-    if limit:
+    if limit is not None:
         query = query.limit(limit)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/db/crud/core_template.py` around lines 108 - 111, The conditionals use
truthiness so limit=0 is treated as False and query.limit(0) is skipped; change
the checks to explicit None checks (e.g., replace "if limit:" with "if limit is
not None:" and similarly "if offset:" with "if offset is not None:") in the
function that builds the query and also apply the identical fix inside
get_core_templates_simple so query.limit(0) and query.offset(0) are honored.

62-64: Silent DB failure in get_core_template_values — no log emitted.

When the SELECT fails with SQLAlchemyError the function silently returns hardcoded defaults. Operators would have no indication that the DB is unreachable and templates have fallen back to defaults. A warning-level log before the return defaults would make this observable.

♻️ Proposed fix
+import logging
+_logger = logging.getLogger(__name__)
 ...
     except SQLAlchemyError:
+        _logger.warning("Failed to fetch core template values from DB; falling back to defaults", exc_info=True)
         return defaults
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/db/crud/core_template.py` around lines 62 - 64, get_core_template_values
currently swallows SQLAlchemyError and returns defaults silently; update the
except block to log a warning before returning. Specifically, in
get_core_template_values catch SQLAlchemyError as e (or similar), call the
module logger (e.g., logger.warning or processLogger.warning) with a clear
message including the exception details and context (e.g., "Failed to fetch core
template values, falling back to defaults: %s" % e), then return defaults.
Ensure you reference SQLAlchemyError, the defaults variable, and the
get_core_template_values function when making the change.

108-111: Use is not None guard for offset and limit to avoid incorrect behavior with limit=0.

if offset: and if limit: treat 0 as falsy. For offset=0 this is harmless (no-op), but limit=0 silently skips the limit and returns all rows instead of zero. The same pattern repeats in get_core_templates_simple (lines 148–151).

♻️ Proposed fix (apply the same change in `get_core_templates_simple` lines 148–151)
-    if offset:
+    if offset is not None:
         query = query.offset(offset)
-    if limit:
+    if limit is not None:
         query = query.limit(limit)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/db/crud/core_template.py` around lines 108 - 111, Change the truthy
checks for offset and limit to explicit None checks so zero values aren't
treated as falsy: replace the "if offset:" and "if limit:" guards with "if
offset is not None:" and "if limit is not None:" in the function containing the
shown query modifications (and apply the same change in
get_core_templates_simple where the same pattern occurs) so a provided limit=0
correctly applies a zero-row limit.
app/models/core_template.py (2)

76-81: Inconsistent total-count field names across list response types.

CoreTemplateResponseList (line 77) calls the total count count, while CoreTemplatesSimpleResponse (line 94) calls it total. API consumers that handle both shapes will need special-case knowledge of which field to read.

♻️ Proposed alignment (unify to `total`)
 class CoreTemplateResponseList(BaseModel):
-    count: int
+    total: int
     templates: list[CoreTemplateResponse] = []

Update the construction in app/operation/core_template.py line 95:

-    return CoreTemplateResponseList(templates=templates, count=count)
+    return CoreTemplateResponseList(templates=templates, total=count)

Also applies to: 92-94

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/core_template.py` around lines 76 - 81, The list response uses
inconsistent total field names: change CoreTemplateResponseList.count to
CoreTemplateResponseList.total (type int) and update any construction/usages
that build instances (e.g., the response created in the code that constructs
CoreTemplateResponseList in the core template operations—previously setting
"count") to set "total" instead; ensure CoreTemplatesSimpleResponse and
CoreTemplateResponseList now share the same field name and adjust any
tests/serializations that read "count" to read "total" (leave model_config
as-is).

76-81: Inconsistent list-response field naming: count vs total.

CoreTemplateResponseList exposes the size field as count (line 77), while CoreTemplatesSimpleResponse calls the same concept total (line 94). API consumers that handle both response shapes will need to know which field name to use, which is error-prone.

♻️ Proposed alignment (pick one name consistently)
 class CoreTemplateResponseList(BaseModel):
-    count: int
+    total: int
     templates: list[CoreTemplateResponse] = []

     model_config = ConfigDict(from_attributes=True)

Then update the construction site in app/operation/core_template.py line 95:

-    return CoreTemplateResponseList(templates=templates, count=count)
+    return CoreTemplateResponseList(templates=templates, total=count)

Also applies to: 92-94

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/core_template.py` around lines 76 - 81, The list response fields
are inconsistent: CoreTemplateResponseList defines the size field as count while
CoreTemplatesSimpleResponse uses total; pick one name (e.g., total) and make
both models use it consistently by renaming the field in
CoreTemplateResponseList (replace count:int with total:int) and adjusting any
default/initialization (templates: list[CoreTemplateResponse] = []). Then update
the code that constructs these DTOs (the place that creates a
CoreTemplateResponseList instance in the core_template operation) to
pass/consume the chosen field name (total) instead of count so both response
shapes match.
tests/api/test_core_template.py (1)

7-26: Tests don't clean up created templates.

None of the test functions delete the templates they create. If the test database isn't reset between runs (or between tests in the same session), leftover templates could cause flaky results in other tests. Consider adding cleanup in a finally block or using a pytest fixture with teardown.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/api/test_core_template.py` around lines 7 - 26, The
test_core_template_create_and_get test creates a template via
create_core_template but never deletes it, risking test pollution; modify the
test to remove the created template in a teardown step (e.g., wrap the
create/assert logic in try/finally and call the API to delete the template by
created["id"] in the finally block) or move creation to a pytest fixture that
yields the created template and deletes it in the fixture teardown, ensuring
cleanup uses the same client/Authorization header used in client.get.
app/routers/core_template.py (3)

66-74: Unconventional route construction using "s" suffix.

The list endpoints are defined as @router.get("s") and @router.get("s/simple"), relying on the prefix /api/core_template to form /api/core_templates and /api/core_templates/simple. While functional, this is non-obvious and could confuse contributors. A more conventional approach would be to define the prefix as /api/core_templates and use "" / "/simple" for list routes, and "/{template_id}" for singular resource routes — or split into two routers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routers/core_template.py` around lines 66 - 74, The route decorators use
an unconventional `"s"` suffix to build plural paths (e.g., `@router.get`("s") on
get_core_templates) which is confusing; update the router prefix to a plural
form (e.g., change the router prefix from "/api/core_template" to
"/api/core_templates") and then change the list route decorators to use "" and
"/simple" (e.g., `@router.get`("") for get_core_templates and
`@router.get`("/simple") for get_core_templates_simple) and ensure singular routes
use "/{template_id}" (e.g., get_core_template_by_id) so path construction is
explicit and readable.

84-84: Parameter all shadows the Python built-in.

Renaming to e.g. fetch_all or include_all avoids shadowing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routers/core_template.py` at line 84, The parameter named "all" shadows
the Python built-in; rename it (e.g., to include_all or fetch_all) in the
function signature where "all: bool = False" appears (and update the parameter
annotation) and then update every usage inside that function and all call sites
to use the new name (also adjust any type hints, default handling, and tests or
docs referencing the old name). Ensure the new identifier is used consistently
in any related methods or templates that forwarded the parameter (search for
"all=" and "all :" occurrences) to avoid runtime NameError or shadowing issues.

56-63: Returning {} from a 204 No Content endpoint.

HTTP 204 semantics require no response body. Returning an empty dict is not idiomatic and may cause some clients or middleware to behave unexpectedly. Return None or use Response(status_code=204) instead.

♻️ Suggested fix
 `@router.delete`("/{template_id}", status_code=status.HTTP_204_NO_CONTENT)
 async def remove_core_template(
     template_id: int,
     db: AsyncSession = Depends(get_db),
     admin: AdminDetails = Depends(check_sudo_admin),
 ):
     await core_template_operator.remove_core_template(db, template_id, admin)
-    return {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/routers/core_template.py` around lines 56 - 63, The delete endpoint
remove_core_template currently returns an empty dict which violates 204 No
Content semantics; update the function (remove_core_template in
app/routers/core_template.py) to not return a body — either return None or
explicitly return a fastapi.Response(status_code=status.HTTP_204_NO_CONTENT)
after calling core_template_operator.remove_core_template(db, template_id,
admin) so no response body is emitted.
app/subscription/clash.py (1)

31-31: Inconsistent None-check pattern: or vs is None.

base.py and singbox.py use if xxx is None: to default template content, but here or is used. With or, an empty string "" would also trigger the fallback (which may be desirable), but it's inconsistent with the sibling classes. Consider aligning to one pattern across all subscription classes for predictable behavior.

♻️ Suggested alignment with other classes
-        self.clash_template_content = clash_template_content or DEFAULT_CLASH_SUBSCRIPTION_TEMPLATE
+        if clash_template_content is None:
+            clash_template_content = DEFAULT_CLASH_SUBSCRIPTION_TEMPLATE
+        self.clash_template_content = clash_template_content
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/subscription/clash.py` at line 31, The current assignment uses
"self.clash_template_content = clash_template_content or
DEFAULT_CLASH_SUBSCRIPTION_TEMPLATE", which treats empty strings as missing and
differs from the "if ... is None:" pattern used in base.py and singbox.py;
change the logic to mirror those classes by checking "if clash_template_content
is None:" and only then set self.clash_template_content =
DEFAULT_CLASH_SUBSCRIPTION_TEMPLATE (otherwise assign the provided value) so
empty-string inputs remain preserved and behavior is consistent with other
subscription classes.
app/subscription/base.py (2)

37-37: Unnecessary explicit del of local variables.

user_agent_data and grpc_user_agent_data are local variables that will be garbage-collected when __init__ returns. The explicit del adds no value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/subscription/base.py` at line 37, Remove the unnecessary explicit
deletion of local variables: delete the statement "del user_agent_data,
grpc_user_agent_data" from the __init__ method so that local variables
user_agent_data and grpc_user_agent_data are allowed to go out of scope
naturally and be garbage-collected without an explicit del.

24-30: No error handling around template rendering/parsing.

If a core template stored in the DB contains invalid Jinja syntax or malformed JSON, render_template_string or json.loads will raise an unhandled exception, breaking subscription generation for all users. While validation occurs at template creation time, consider wrapping these in a try/except that falls back to the default template and logs a warning, so a corrupt template doesn't take down the subscription endpoint.

🛡️ Suggested defensive fallback
+        import logging
+        logger = logging.getLogger(__name__)
+
         self.proxy_remarks = []
-        user_agent_data = json.loads(render_template_string(user_agent_template_content))
+        try:
+            user_agent_data = json.loads(render_template_string(user_agent_template_content))
+        except Exception:
+            logger.warning("Failed to render user-agent template, falling back to default")
+            user_agent_data = json.loads(render_template_string(DEFAULT_USER_AGENT_TEMPLATE))
         if "list" in user_agent_data and isinstance(user_agent_data["list"], list):
             self.user_agent_list = user_agent_data["list"]
         else:
             self.user_agent_list = []

-        grpc_user_agent_data = json.loads(render_template_string(grpc_user_agent_template_content))
+        try:
+            grpc_user_agent_data = json.loads(render_template_string(grpc_user_agent_template_content))
+        except Exception:
+            logger.warning("Failed to render gRPC user-agent template, falling back to default")
+            grpc_user_agent_data = json.loads(render_template_string(DEFAULT_GRPC_USER_AGENT_TEMPLATE))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/subscription/base.py` around lines 24 - 30, The template rendering and
JSON parsing calls (render_template_string + json.loads) for
user_agent_template_content and grpc_user_agent_template_content can raise and
crash subscription generation; wrap each render/json.loads block in a try/except
that catches Exception, logs a warning via the class logger (e.g.,
self.logger.warning or whatever logger is used in this class) including which
template failed and the exception, and fall back to safe defaults (set
self.user_agent_list = [] and grpc_user_agent_data = {} or a default template
result) so a corrupt template doesn't break the subscription endpoint; apply
this defensive pattern around both the user_agent and grpc_user_agent processing
in the same method.
app/subscription/share.py (2)

75-76: No error handling if subscription_core_templates() fails.

If the DB/cache call fails, the exception will propagate directly to the HTTP handler. Since this is a user-facing subscription endpoint, consider a try/except with a fallback to default templates so that subscription generation remains available even during transient DB issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/subscription/share.py` around lines 75 - 76, Wrap the call to
subscription_core_templates() in a try/except to catch any exceptions, log the
error (e.g., logging.exception or processLogger.error) and set a safe fallback
for core_templates (for example a default templates list or an empty set) so
that _build_subscription_config(config_format, core_templates) still runs and
returns a usable conf; ensure you reference subscription_core_templates(),
core_templates and _build_subscription_config in the change and do not let the
original exception propagate to the HTTP handler.

37-65: Hard-coded dict keys are fragile — consider using constants.

The template keys ("USER_AGENT_TEMPLATE", "GRPC_USER_AGENT_TEMPLATE", "XRAY_SUBSCRIPTION_TEMPLATE", "SINGBOX_SUBSCRIPTION_TEMPLATE", "CLASH_SUBSCRIPTION_TEMPLATE") are raw string literals. A typo in any of them will produce a KeyError at runtime with no compile-time safety. Define these as module-level constants (or reference an enum) so they're defined once and shared with subscription_core_templates().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/subscription/share.py` around lines 37 - 65, Replace the raw string
template keys in _build_subscription_config with module-level constants (e.g.,
USER_AGENT_TEMPLATE, GRPC_USER_AGENT_TEMPLATE, XRAY_SUBSCRIPTION_TEMPLATE,
SINGBOX_SUBSCRIPTION_TEMPLATE, CLASH_SUBSCRIPTION_TEMPLATE) and use those
constants when constructing common_kwargs and when passing template_*_content
into StandardLinks, ClashMetaConfiguration, SingBoxConfiguration,
XrayConfiguration, and OutlineConfiguration; also update the provider of
core_templates (subscription_core_templates) to populate and export the same
constants so both functions reference the single source of truth and avoid
KeyError from mistyped literal keys.
app/subscription/singbox.py (1)

19-31: Same lack of error handling as base.py for template rendering.

json.loads(render_template_string(singbox_template_content)) on Line 31 will raise an unhandled exception if the template content contains invalid Jinja or malformed JSON. Consider wrapping with a try/except that falls back to DEFAULT_SINGBOX_SUBSCRIPTION_TEMPLATE, consistent with the recommendation for base.py.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/subscription/singbox.py` around lines 19 - 31, Wrap the template
rendering and JSON parsing in SingboxSubscription.__init__ in a try/except
around the call to json.loads(render_template_string(singbox_template_content))
so template/Jinja/JSON errors are caught; on exception, fall back to parsing
DEFAULT_SINGBOX_SUBSCRIPTION_TEMPLATE and assign to self.config (and optionally
log the error), keeping render_template_string, json.loads,
DEFAULT_SINGBOX_SUBSCRIPTION_TEMPLATE and self.config as the referenced symbols
to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/db/migrations/versions/e8c6a4f1d2b7_add_core_templates_table.py`:
- Line 350: The gRPC User-Agent template string in the migration
e8c6a4f1d2b7_add_core_templates_table.py contains a stray ']' character inside
the JSON/seed value ("grpc-ruby/1.62.0 grpc-c/39.0.0 (osx; chttp2)]"); remove
the trailing ']' so the seeded string becomes "grpc-ruby/1.62.0 grpc-c/39.0.0
(osx; chttp2)" and update the value where it is assigned (the core
templates/default templates list in that migration) so the database seeds with
the corrected User-Agent.
- Line 350: The seeded User-Agent string in the migration
e8c6a4f1d2b7_add_core_templates_table contains a stray trailing bracket — the
value `"grpc-ruby/1.62.0 grpc-c/39.0.0 (osx; chttp2)]"` — which corrupts
persisted data; remove the trailing `]` so the seeded value becomes
`"grpc-ruby/1.62.0 grpc-c/39.0.0 (osx; chttp2)"` in the migration’s INSERT/seed
payload (leave all surrounding JSON formatting intact) and re-run the
migration/seed so the corrected string is stored.

In `@app/operation/core_template.py`:
- Around line 172-179: The deletion of a core template is being committed inside
remove_core_template while the default promotion happens afterwards in
set_default_template, which can leave the type without a default on failure;
change the flow so both delete and promotion occur in a single DB transaction:
either modify remove_core_template to only call db.delete() without committing
and let the caller (the code around get_first_template_by_type /
set_default_template) perform the transaction/commit, or implement a new atomic
helper (e.g., remove_core_template_and_promote_default) that runs delete +
set_default_template logic inside one transactional unit and commits once both
succeed; update callers to use the new transactional behavior and ensure no
intermediate commit happens in remove_core_template.
- Around line 172-179: The deletion commits separately causing a window where a
failure in promotion leaves the type without a default; change the flow so
deletion and promotion occur in one transaction: update remove_core_template to
only call db.delete(db_template) (no db.commit()) or add a non-committing
variant, then in the caller (the function handling db_template removal) call
get_first_template_by_type (if db_template.is_default) and set_default_template
as needed, and finally perform a single db.commit() once both deletion and
default promotion succeed; references: remove_core_template,
set_default_template, get_first_template_by_type, db_template, replacement.
- Around line 46-66: The JSON validation branch in _validate_template_content
calls render_template_string(content) with no context so undefined Jinja
variables render to empty strings and json.loads can fail; update the JSON
branch in _validate_template_content to call render_template_string with a
minimal stub context matching the JSON template kinds (e.g., provide keys like
outbounds: [], inbounds: [], proxies: [], proxy-groups: [], rules: [],
proxy_remarks: [] as appropriate) before json.loads, or alternatively skip
rendering and validate raw JSON structure first; ensure changes reference
CoreTemplateType and keep the existing parsed-type check (e.g., for
CoreTemplateType.user_agent and CoreTemplateType.grpc_user_agent) so templates
still must render to a JSON object.
- Around line 46-66: The JSON template validation in _validate_template_content
is inconsistent: only CoreTemplateType.clash_subscription gets a stub Jinja2
context before rendering, causing other types (e.g., Xray, SingBox, user_agent,
grpc_user_agent) to fail on templates that reference variables; update the
render call(s) inside _validate_template_content so all template types use an
appropriate mock context (e.g., the same {"conf": {"proxies": [],
"proxy-groups": [], "rules": []}, "proxy_remarks": []} or a minimal common stub)
when calling render_template_string for non-clash types, then parse the rendered
output (yaml.safe_load or json.loads as appropriate) and keep the existing
exception handling and raise_error behavior.

In `@app/subscription/share.py`:
- Around line 58-59: OutlineConfiguration currently subclasses BaseSubscription
but its __init__ does not call super().__init__, leaving the parent
uninitialized; either remove the inheritance or update
OutlineConfiguration.__init__ to call
super().__init__(user_agent_template_content, grpc_user_agent_template_content)
and properly store/handle those template args, and update the instantiation site
(the branch that returns OutlineConfiguration()) to pass the needed template
content variables so BaseSubscription is initialized correctly; refer to the
OutlineConfiguration class and BaseSubscription.__init__ for where to add the
super call and to the place that constructs OutlineConfiguration to supply the
two template parameters.

In `@app/templates/__init__.py`:
- Around line 24-25: The render_template_string function currently calls
env.from_string(template_content) which renders untrusted DB-sourced templates
with an unsandboxed Jinja2 Environment; replace this with a
SandboxedEnvironment-based render: create or use
jinja2.sandbox.SandboxedEnvironment to parse and render template_content
(preserving the same context fallback logic), so that render_template_string
uses SandboxedEnvironment.from_string(...).render(context or {}) instead of
env.from_string(...).render(...), ensuring the template execution is sandboxed
for defense-in-depth while keeping the function signature and context handling
intact.

In `@tests/api/test_core_template.py`:
- Around line 29-54: The test test_core_template_can_switch_default is brittle
because DB migrations may seed a default template for template_type
"singbox_subscription"; instead first query any existing default for that type
(e.g., GET
/api/core_template?template_type=singbox_subscription&is_default=true) or create
a template and explicitly set it as default via create_core_template to
guarantee a known previous default, then create the second template with
is_default=True using create_core_template and finally fetch both templates
(using client.get on the IDs created) to assert the original default is now
is_default False and the new one is True; update references to
create_core_template, test_core_template_can_switch_default, and the client.get
checks accordingly.
- Around line 57-88: The test test_core_template_last_and_system_delete_guards
assumes no seeded templates for template_type "grpc_user_agent"; update the test
to avoid seeded types by using a unique/non-seeded template_type (e.g., change
template_type passed to create_core_template in that test) or first query
existing templates for that type and adjust the expected responses accordingly
so the delete-guard assertions reflect whether a system/last template already
exists; modify references in the test function
(test_core_template_last_and_system_delete_guards and the two
create_core_template calls) to use the new non-seeded type or include a
pre-check that accounts for seeded data before asserting 403/204.

---

Outside diff comments:
In `@app/subscription/singbox.py`:
- Around line 56-72: In render(), the list comprehensions building urltest_tags
and selector_tags access outbound["type"] which raises KeyError for outbounds
missing the "type" key; change those to use outbound.get("type") (e.g., in the
comprehensions that create urltest_tags and selector_tags) so malformed entries
are skipped safely and behavior remains the same; adjust any similar direct dict
indexing in render() to .get("type") to prevent the crash.

---

Nitpick comments:
In `@app/db/crud/core_template.py`:
- Around line 134-141: The tuple branch is dead because
CoreTemplateSortingOptionsSimple items yield SQLAlchemy UnaryExpression objects,
not tuples; update the sorting logic in core_template.py to always collect
s.value (e.g., replace the isinstance branch with a simple comprehension or
single append per item) so sort_list becomes [s.value for s in sort] and then
call stmt = stmt.order_by(*sort_list); reference symbols: sort, sort_list,
s.value, and stmt.order_by.
- Around line 134-141: The branch checking isinstance(s.value, tuple) is dead
because CoreTemplateSortingOptionsSimple produces SQLAlchemy UnaryExpression
objects (from .asc()/.desc()), so remove the unreachable tuple branch and
simplify the loop in the sort handling: build sort_list by appending s.value for
each s in sort and then call stmt.order_by(*sort_list); update the block around
sort, sort_list, s.value, and stmt.order_by accordingly to eliminate the
isinstance(...) branch.
- Around line 62-64: The catch in get_core_template_values currently swallows
SQLAlchemyError and returns defaults silently; update the function to log a
warning with the exception details before returning defaults by obtaining/using
a module-level logger (e.g. define logger = get_logger("core-template-crud") at
top or reuse an existing logger) and change the except block to log the
error/stack (include the exception object) and then return defaults so operators
can see DB failures.
- Around line 108-111: The conditionals use truthiness so limit=0 is treated as
False and query.limit(0) is skipped; change the checks to explicit None checks
(e.g., replace "if limit:" with "if limit is not None:" and similarly "if
offset:" with "if offset is not None:") in the function that builds the query
and also apply the identical fix inside get_core_templates_simple so
query.limit(0) and query.offset(0) are honored.
- Around line 62-64: get_core_template_values currently swallows SQLAlchemyError
and returns defaults silently; update the except block to log a warning before
returning. Specifically, in get_core_template_values catch SQLAlchemyError as e
(or similar), call the module logger (e.g., logger.warning or
processLogger.warning) with a clear message including the exception details and
context (e.g., "Failed to fetch core template values, falling back to defaults:
%s" % e), then return defaults. Ensure you reference SQLAlchemyError, the
defaults variable, and the get_core_template_values function when making the
change.
- Around line 108-111: Change the truthy checks for offset and limit to explicit
None checks so zero values aren't treated as falsy: replace the "if offset:" and
"if limit:" guards with "if offset is not None:" and "if limit is not None:" in
the function containing the shown query modifications (and apply the same change
in get_core_templates_simple where the same pattern occurs) so a provided
limit=0 correctly applies a zero-row limit.

In `@app/db/models.py`:
- Around line 717-729: CoreTemplate is missing a created_at timestamp—add a
Mapped[datetime] mapped_column named created_at to the CoreTemplate model (class
CoreTemplate) so creation time is recorded; implement it with a non-nullable
DateTime column using a server default (e.g., func.now()) to auto-populate, and
ensure you import DateTime and func from sqlalchemy if not already present.

In `@app/models/core_template.py`:
- Around line 76-81: The list response uses inconsistent total field names:
change CoreTemplateResponseList.count to CoreTemplateResponseList.total (type
int) and update any construction/usages that build instances (e.g., the response
created in the code that constructs CoreTemplateResponseList in the core
template operations—previously setting "count") to set "total" instead; ensure
CoreTemplatesSimpleResponse and CoreTemplateResponseList now share the same
field name and adjust any tests/serializations that read "count" to read "total"
(leave model_config as-is).
- Around line 76-81: The list response fields are inconsistent:
CoreTemplateResponseList defines the size field as count while
CoreTemplatesSimpleResponse uses total; pick one name (e.g., total) and make
both models use it consistently by renaming the field in
CoreTemplateResponseList (replace count:int with total:int) and adjusting any
default/initialization (templates: list[CoreTemplateResponse] = []). Then update
the code that constructs these DTOs (the place that creates a
CoreTemplateResponseList instance in the core_template operation) to
pass/consume the chosen field name (total) instead of count so both response
shapes match.

In `@app/routers/core_template.py`:
- Around line 66-74: The route decorators use an unconventional `"s"` suffix to
build plural paths (e.g., `@router.get`("s") on get_core_templates) which is
confusing; update the router prefix to a plural form (e.g., change the router
prefix from "/api/core_template" to "/api/core_templates") and then change the
list route decorators to use "" and "/simple" (e.g., `@router.get`("") for
get_core_templates and `@router.get`("/simple") for get_core_templates_simple) and
ensure singular routes use "/{template_id}" (e.g., get_core_template_by_id) so
path construction is explicit and readable.
- Line 84: The parameter named "all" shadows the Python built-in; rename it
(e.g., to include_all or fetch_all) in the function signature where "all: bool =
False" appears (and update the parameter annotation) and then update every usage
inside that function and all call sites to use the new name (also adjust any
type hints, default handling, and tests or docs referencing the old name).
Ensure the new identifier is used consistently in any related methods or
templates that forwarded the parameter (search for "all=" and "all :"
occurrences) to avoid runtime NameError or shadowing issues.
- Around line 56-63: The delete endpoint remove_core_template currently returns
an empty dict which violates 204 No Content semantics; update the function
(remove_core_template in app/routers/core_template.py) to not return a body —
either return None or explicitly return a
fastapi.Response(status_code=status.HTTP_204_NO_CONTENT) after calling
core_template_operator.remove_core_template(db, template_id, admin) so no
response body is emitted.

In `@app/subscription/base.py`:
- Line 37: Remove the unnecessary explicit deletion of local variables: delete
the statement "del user_agent_data, grpc_user_agent_data" from the __init__
method so that local variables user_agent_data and grpc_user_agent_data are
allowed to go out of scope naturally and be garbage-collected without an
explicit del.
- Around line 24-30: The template rendering and JSON parsing calls
(render_template_string + json.loads) for user_agent_template_content and
grpc_user_agent_template_content can raise and crash subscription generation;
wrap each render/json.loads block in a try/except that catches Exception, logs a
warning via the class logger (e.g., self.logger.warning or whatever logger is
used in this class) including which template failed and the exception, and fall
back to safe defaults (set self.user_agent_list = [] and grpc_user_agent_data =
{} or a default template result) so a corrupt template doesn't break the
subscription endpoint; apply this defensive pattern around both the user_agent
and grpc_user_agent processing in the same method.

In `@app/subscription/clash.py`:
- Line 31: The current assignment uses "self.clash_template_content =
clash_template_content or DEFAULT_CLASH_SUBSCRIPTION_TEMPLATE", which treats
empty strings as missing and differs from the "if ... is None:" pattern used in
base.py and singbox.py; change the logic to mirror those classes by checking "if
clash_template_content is None:" and only then set self.clash_template_content =
DEFAULT_CLASH_SUBSCRIPTION_TEMPLATE (otherwise assign the provided value) so
empty-string inputs remain preserved and behavior is consistent with other
subscription classes.

In `@app/subscription/core_templates.py`:
- Around line 7-10: The cached subscription_core_templates function currently
uses `@cached`() with no ttl so values never expire; add a modest TTL (e.g.,
ttl=300 or ttl=600) to the `@cached` decorator on subscription_core_templates to
ensure the cache auto-invalidates periodically as a safety net if NATS messages
to handle_core_template_message are missed or delayed, while keeping
get_core_template_values and the existing manual invalidation logic intact (use
the same decorator signature where subscription_core_templates is defined).

In `@app/subscription/default_templates.py`:
- Around line 18-22: The default template contains hardcoded region-specific DNS
entries under the key "proxy-server-nameserver" (values 178.22.122.100 and
78.157.42.100); update the template in app/subscription/default_templates.py to
use globally appropriate defaults (for example 1.1.1.1 and 8.8.8.8) or replace
these values with clearly documented region-specific placeholders and add a
comment/README note indicating they are Iran-specific so operators can override
them; locate the "nameserver" and "proxy-server-nameserver" entries in the
template and change the values or add the documentation accordingly.

In `@app/subscription/share.py`:
- Around line 75-76: Wrap the call to subscription_core_templates() in a
try/except to catch any exceptions, log the error (e.g., logging.exception or
processLogger.error) and set a safe fallback for core_templates (for example a
default templates list or an empty set) so that
_build_subscription_config(config_format, core_templates) still runs and returns
a usable conf; ensure you reference subscription_core_templates(),
core_templates and _build_subscription_config in the change and do not let the
original exception propagate to the HTTP handler.
- Around line 37-65: Replace the raw string template keys in
_build_subscription_config with module-level constants (e.g.,
USER_AGENT_TEMPLATE, GRPC_USER_AGENT_TEMPLATE, XRAY_SUBSCRIPTION_TEMPLATE,
SINGBOX_SUBSCRIPTION_TEMPLATE, CLASH_SUBSCRIPTION_TEMPLATE) and use those
constants when constructing common_kwargs and when passing template_*_content
into StandardLinks, ClashMetaConfiguration, SingBoxConfiguration,
XrayConfiguration, and OutlineConfiguration; also update the provider of
core_templates (subscription_core_templates) to populate and export the same
constants so both functions reference the single source of truth and avoid
KeyError from mistyped literal keys.

In `@app/subscription/singbox.py`:
- Around line 19-31: Wrap the template rendering and JSON parsing in
SingboxSubscription.__init__ in a try/except around the call to
json.loads(render_template_string(singbox_template_content)) so
template/Jinja/JSON errors are caught; on exception, fall back to parsing
DEFAULT_SINGBOX_SUBSCRIPTION_TEMPLATE and assign to self.config (and optionally
log the error), keeping render_template_string, json.loads,
DEFAULT_SINGBOX_SUBSCRIPTION_TEMPLATE and self.config as the referenced symbols
to locate the change.

In `@tests/api/test_core_template.py`:
- Around line 7-26: The test_core_template_create_and_get test creates a
template via create_core_template but never deletes it, risking test pollution;
modify the test to remove the created template in a teardown step (e.g., wrap
the create/assert logic in try/finally and call the API to delete the template
by created["id"] in the finally block) or move creation to a pytest fixture that
yields the created template and deletes it in the fixture teardown, ensuring
cleanup uses the same client/Authorization header used in client.get.

Comment on lines 46 to 66
async def _validate_template_content(self, template_type: CoreTemplateType, content: str) -> None:
try:
if template_type == CoreTemplateType.clash_subscription:
rendered = render_template_string(
content,
{
"conf": {"proxies": [], "proxy-groups": [], "rules": []},
"proxy_remarks": [],
},
)
yaml.safe_load(rendered)
return

rendered = render_template_string(content)
parsed = json.loads(rendered)
if template_type in (CoreTemplateType.user_agent, CoreTemplateType.grpc_user_agent) and not isinstance(
parsed, dict
):
raise ValueError("User-Agent template content must render to a JSON object")
except Exception as exc:
await self.raise_error(message=f"Invalid template content: {str(exc)}", code=400)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find render_template_string definition in app/templates/
find app/templates -type f -name "*.py" | head -20

Repository: PasarGuard/panel

Length of output: 110


🏁 Script executed:

# Look for the render_template_string function definition
rg -n "def render_template_string" --type py

Repository: PasarGuard/panel

Length of output: 181


🏁 Script executed:

# Search for Jinja2 undefined mode configuration
rg -n "undefined|StrictUndefined|ChainableUndefined|DebugUndefined" --type py app/templates/

Repository: PasarGuard/panel

Length of output: 42


🏁 Script executed:

cat -n app/templates/__init__.py

Repository: PasarGuard/panel

Length of output: 1067


🏁 Script executed:

sed -n '46,66p' app/operation/core_template.py

Repository: PasarGuard/panel

Length of output: 1056


JSON template validation skips context — valid templates using undefined variables will be silently rejected.

The Clash branch (lines 49–55) passes a mock context to render_template_string, ensuring template variables resolve correctly. The JSON branch (line 59) calls render_template_string(content) with no context, resulting in an empty dict. Since the Jinja2 Environment uses default undefined mode (app/templates/init.py:15), undefined variables render as empty strings. For custom Xray or SingBox templates using any undefined variable (e.g., {{ outbounds | tojson }}), this produces malformed JSON that fails json.loads, incorrectly rejecting valid templates.

Provide minimal stub contexts for each JSON type (e.g., an empty outbounds list for Xray/SingBox) — mirroring the Clash approach — or skip validation entirely for JSON types and validate raw JSON structure before rendering.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/operation/core_template.py` around lines 46 - 66, The JSON validation
branch in _validate_template_content calls render_template_string(content) with
no context so undefined Jinja variables render to empty strings and json.loads
can fail; update the JSON branch in _validate_template_content to call
render_template_string with a minimal stub context matching the JSON template
kinds (e.g., provide keys like outbounds: [], inbounds: [], proxies: [],
proxy-groups: [], rules: [], proxy_remarks: [] as appropriate) before
json.loads, or alternatively skip rendering and validate raw JSON structure
first; ensure changes reference CoreTemplateType and keep the existing
parsed-type check (e.g., for CoreTemplateType.user_agent and
CoreTemplateType.grpc_user_agent) so templates still must render to a JSON
object.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Jinja2 default behavior undefined variables UndefinedError render empty string

💡 Result:

In Jinja2’s default configuration, missing names/attributes resolve to an instance of jinja2.Undefined:

  • {{ missing }} renders as an empty string ('') and is falsy.
  • But many other operations on it (math, string ops, etc.) will raise jinja2.exceptions.UndefinedError.
    This is the documented behavior of Undefined. [1]

If you want an error instead of rendering '', switch to StrictUndefined:

from jinja2 import Environment, StrictUndefined
env = Environment(undefined=StrictUndefined)

(or in Flask: app.jinja_env.undefined = StrictUndefined). [1][2]

If you want to keep default undefined behavior but provide a fallback value in the template, use the default filter:

{{ missing | default('') }}

(default returns the fallback when the value is undefined; it can also be configured to treat falsy values as needing the default via its second parameter). [3]

Sources:
[1] Jinja “API” docs – Undefined / StrictUndefined behavior and UndefinedError
[2] Stack Overflow – setting Flask to StrictUndefined
[3] Jinja “Template Designer Documentation” – default filter behavior


🏁 Script executed:

fd -type f -name "*.py" | xargs rg "def render_template_string" -A 20

Repository: PasarGuard/panel

Length of output: 459


🏁 Script executed:

fd -type f -name "*.py" | xargs rg "Environment\(" -B 2 -A 10 | head -100

Repository: PasarGuard/panel

Length of output: 1109


🏁 Script executed:

rg "from jinja2|import.*Jinja" --type py

Repository: PasarGuard/panel

Length of output: 42


🏁 Script executed:

rg "CoreTemplateType\.(xray|singbox|user_agent|grpc_user_agent)" -B 2 -A 2 | head -50

Repository: PasarGuard/panel

Length of output: 1110


🏁 Script executed:

fd -type f -name "*.py" | xargs rg "def.*test.*template|def.*test.*validate" | head -20

Repository: PasarGuard/panel

Length of output: 1992


🏁 Script executed:

rg "clash_subscription|user_agent.*template" -B 3 -A 3 | head -80

Repository: PasarGuard/panel

Length of output: 5113


🏁 Script executed:

cat tests/api/test_core_template.py

Repository: PasarGuard/panel

Length of output: 2851


🏁 Script executed:

rg "DEFAULT_.*TEMPLATE|DEFAULT_XRAY|DEFAULT_SINGBOX|DEFAULT_USER_AGENT" -A 5

Repository: PasarGuard/panel

Length of output: 12861


JSON template validation inconsistent with Clash—other types lack stub context for Jinja2 variables.

_validate_template_content renders Clash templates with mock context (lines 49–55) to properly resolve Jinja2 variables. For Xray, SingBox, and user-agent types (lines 59–64), render_template_string(content) is called with no context. If a custom template contains Jinja2 expressions (e.g., {{ proxies | tojson }}), undefined variables render as empty strings or raise UndefinedError, producing invalid JSON and incorrectly rejecting the template. The render infrastructure is present for all types, so templates should either all receive mock/stub context to be consistent with Clash, or validation should be relaxed to check raw JSON structure before rendering.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/operation/core_template.py` around lines 46 - 66, The JSON template
validation in _validate_template_content is inconsistent: only
CoreTemplateType.clash_subscription gets a stub Jinja2 context before rendering,
causing other types (e.g., Xray, SingBox, user_agent, grpc_user_agent) to fail
on templates that reference variables; update the render call(s) inside
_validate_template_content so all template types use an appropriate mock context
(e.g., the same {"conf": {"proxies": [], "proxy-groups": [], "rules": []},
"proxy_remarks": []} or a minimal common stub) when calling
render_template_string for non-clash types, then parse the rendered output
(yaml.safe_load or json.loads as appropriate) and keep the existing exception
handling and raise_error behavior.

Comment on lines 172 to 179
replacement = None
if db_template.is_default:
replacement = await get_first_template_by_type(db, template_type, exclude_id=db_template.id)

await remove_core_template(db, db_template)

if replacement is not None:
await set_default_template(db, replacement)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Delete and default-promotion are in separate transactions — orphaned state on failure.

remove_core_template (line 176) commits the deletion immediately. If set_default_template (line 179) subsequently fails (e.g. transient DB error), the type has no default template until a manual fix. Combine both operations in a single transaction: have remove_core_template only issue a db.delete() without committing, and perform the set_default_template logic in the same unit of work before the shared commit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/operation/core_template.py` around lines 172 - 179, The deletion of a
core template is being committed inside remove_core_template while the default
promotion happens afterwards in set_default_template, which can leave the type
without a default on failure; change the flow so both delete and promotion occur
in a single DB transaction: either modify remove_core_template to only call
db.delete() without committing and let the caller (the code around
get_first_template_by_type / set_default_template) perform the
transaction/commit, or implement a new atomic helper (e.g.,
remove_core_template_and_promote_default) that runs delete +
set_default_template logic inside one transactional unit and commits once both
succeed; update callers to use the new transactional behavior and ensure no
intermediate commit happens in remove_core_template.

⚠️ Potential issue | 🟡 Minor

Delete and default-promotion are in separate transactions — leaves type without a default on failure.

remove_core_template (line 176) calls db.commit() inside the CRUD function, so the deletion is permanently committed. If set_default_template (line 179) subsequently fails (e.g. transient DB error during its own db.commit()), the type is left with no default template and no automatic recovery path.

Refactor both steps into a single unit of work: have remove_core_template issue db.delete() without committing, then perform the default-promotion before the shared final commit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/operation/core_template.py` around lines 172 - 179, The deletion commits
separately causing a window where a failure in promotion leaves the type without
a default; change the flow so deletion and promotion occur in one transaction:
update remove_core_template to only call db.delete(db_template) (no db.commit())
or add a non-committing variant, then in the caller (the function handling
db_template removal) call get_first_template_by_type (if db_template.is_default)
and set_default_template as needed, and finally perform a single db.commit()
once both deletion and default promotion succeed; references:
remove_core_template, set_default_template, get_first_template_by_type,
db_template, replacement.

Comment on lines +58 to +59
if config_format == "outline":
return OutlineConfiguration()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and examine OutlineConfiguration class definition and its parent
rg -n 'class OutlineConfiguration' --type=py -A 15

Repository: PasarGuard/panel

Length of output: 1118


🏁 Script executed:

# Check the share.py file for all config instantiations to compare
rg -n 'Configuration\(\)' app/subscription/share.py

Repository: PasarGuard/panel

Length of output: 100


🏁 Script executed:

# Examine the BaseSubscription class to understand template handling
rg -n 'class BaseSubscription' --type=py -A 20

Repository: PasarGuard/panel

Length of output: 1592


🏁 Script executed:

# Look at all config instantiation patterns in the file
sed -n '40,80p' app/subscription/share.py

Repository: PasarGuard/panel

Length of output: 1608


🏁 Script executed:

# View the complete OutlineConfiguration class to check for super().__init__() call
cat app/subscription/outline.py

Repository: PasarGuard/panel

Length of output: 1487


🏁 Script executed:

# Check if OutlineConfiguration uses any BaseSubscription attributes like user_agent_list
rg -n 'self\.user_agent' app/subscription/outline.py
rg -n 'self\.proxy_remarks' app/subscription/outline.py
rg -n 'self\.grpc_user_agent' app/subscription/outline.py

Repository: PasarGuard/panel

Length of output: 42


🏁 Script executed:

# Check the full function signature and context of _build_subscription_config
sed -n '45,70p' app/subscription/share.py | cat -n

Repository: PasarGuard/panel

Length of output: 1098


OutlineConfiguration doesn't call super().__init__() — this is a design issue.

OutlineConfiguration extends BaseSubscription but its __init__() never calls the parent constructor. While it's instantiated bare without template parameters in share.py line 59 (unlike all other config types), the actual problem is deeper: OutlineConfiguration doesn't initialize BaseSubscription at all. This creates misleading inheritance — either OutlineConfiguration should not extend BaseSubscription, or it should call super().__init__(user_agent_template_content, grpc_user_agent_template_content) and handle templates properly. Currently, it works only because OutlineConfiguration doesn't use any parent class attributes, but this is fragile design.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/subscription/share.py` around lines 58 - 59, OutlineConfiguration
currently subclasses BaseSubscription but its __init__ does not call
super().__init__, leaving the parent uninitialized; either remove the
inheritance or update OutlineConfiguration.__init__ to call
super().__init__(user_agent_template_content, grpc_user_agent_template_content)
and properly store/handle those template args, and update the instantiation site
(the branch that returns OutlineConfiguration()) to pass the needed template
content variables so BaseSubscription is initialized correctly; refer to the
OutlineConfiguration class and BaseSubscription.__init__ for where to add the
super call and to the place that constructs OutlineConfiguration to supply the
two template parameters.

Comment on lines +24 to +25
def render_template_string(template_content: str, context: Union[dict, None] = None) -> str:
return env.from_string(template_content).render(context or {})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, view the actual app/templates/__init__.py file to see the env setup and function definition
cat -n app/templates/__init__.py

Repository: PasarGuard/panel

Length of output: 1067


🏁 Script executed:

# Search for all calls to render_template_string to understand usage patterns
rg -n 'render_template_string' --type=py -B 2 -A 2

Repository: PasarGuard/panel

Length of output: 4534


🏁 Script executed:

# Check the CoreTemplate model definition to understand data flow
rg -n 'class CoreTemplate' --type=py -A 15

Repository: PasarGuard/panel

Length of output: 6503


🏁 Script executed:

# Look at the admin API endpoint that modifies template content
rg -n 'core_template' app/routers/ --type=py -B 2 -A 5

Repository: PasarGuard/panel

Length of output: 6574


🏁 Script executed:

# Check the check_sudo_admin implementation
rg -n 'def check_sudo_admin' --type=py -A 10

Repository: PasarGuard/panel

Length of output: 996


🏁 Script executed:

# Search for DEFAULT_*_TEMPLATE definitions and usage
rg -n 'DEFAULT_.*TEMPLATE' --type=py -B 2 -A 2

Repository: PasarGuard/panel

Length of output: 10574


🏁 Script executed:

# Check if CoreTemplate content can be modified outside the router API
rg -n 'CoreTemplate' --type=py | grep -i 'update\|modify\|content.*='

Repository: PasarGuard/panel

Length of output: 793


Server-Side Template Injection (SSTI) risk with unsandboxed Jinja2 environment.

The SSTI vulnerability is valid: env.from_string(template_content) renders database-sourced template content through an unsandboxed Jinja2 Environment, allowing arbitrary Python code execution via template payloads (e.g., {{ ''.__class__.__mro__... }}).

However, note that CoreTemplate content can only be modified by sudo admins (all create/modify endpoints require check_sudo_admin). The risk exists but is narrower in scope than general admin escalation—it requires either a compromised sudo admin account or a malicious insider with sudo privileges.

Consider using a SandboxedEnvironment for defense-in-depth:

🔒 Proposed fix using SandboxedEnvironment
+from jinja2.sandbox import SandboxedEnvironment
+
+sandboxed_env = SandboxedEnvironment()
+sandboxed_env.filters.update(CUSTOM_FILTERS)
+sandboxed_env.globals["now"] = lambda: dt.now(tz.utc)
+
+
 def render_template_string(template_content: str, context: Union[dict, None] = None) -> str:
-    return env.from_string(template_content).render(context or {})
+    return sandboxed_env.from_string(template_content).render(context or {})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def render_template_string(template_content: str, context: Union[dict, None] = None) -> str:
return env.from_string(template_content).render(context or {})
from jinja2.sandbox import SandboxedEnvironment
sandboxed_env = SandboxedEnvironment()
sandboxed_env.filters.update(CUSTOM_FILTERS)
sandboxed_env.globals["now"] = lambda: dt.now(tz.utc)
def render_template_string(template_content: str, context: Union[dict, None] = None) -> str:
return sandboxed_env.from_string(template_content).render(context or {})
🧰 Tools
🪛 GitHub Check: CodeQL

[failure] 25-25: Server Side Template Injection
This template construction depends on a user-provided value.
This template construction depends on a user-provided value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/templates/__init__.py` around lines 24 - 25, The render_template_string
function currently calls env.from_string(template_content) which renders
untrusted DB-sourced templates with an unsandboxed Jinja2 Environment; replace
this with a SandboxedEnvironment-based render: create or use
jinja2.sandbox.SandboxedEnvironment to parse and render template_content
(preserving the same context fallback logic), so that render_template_string
uses SandboxedEnvironment.from_string(...).render(context or {}) instead of
env.from_string(...).render(...), ensuring the template execution is sandboxed
for defense-in-depth while keeping the function signature and context handling
intact.

@ImMohammad20000 ImMohammad20000 marked this pull request as ready for review February 25, 2026 17:47
@ImMohammad20000 ImMohammad20000 changed the title feat: implement core template management system feat: implement client template management system Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant